/*******************************************************************************
* Copyright (c) 2010 Stefan A. Tzeggai.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v2.1
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* Stefan A. Tzeggai - initial API and implementation
******************************************************************************/
package org.geopublishing.atlasStyler.classification;
import hep.aida.bin.DynamicBin1D;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import org.apache.log4j.Logger;
import org.geopublishing.atlasStyler.ASUtil;
import org.geopublishing.atlasStyler.classification.ClassificationChangeEvent.CHANGETYPES;
import org.geotools.data.DefaultQuery;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.statistics.HistogramDataset;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.TextAnchor;
import org.opengis.feature.Attribute;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import cern.colt.list.DoubleArrayList;
import de.schmitzm.geotools.data.amd.AttributeMetadataImpl;
import de.schmitzm.geotools.feature.FeatureUtil;
import de.schmitzm.geotools.styling.StyledFeaturesInterface;
import de.schmitzm.lang.LangUtil;
import de.schmitzm.lang.LimitedHashMap;
/**
* A quantitative classification. The inveralls are defined by upper and lower
* limits
*
*
* @param <T>
* The type of the value field
*
* @author stefan
*/
public class FeatureClassification extends Classification {
/**
* This CONSTANT is only used in the JCombobox. NORMALIZER_FIELD String is
* null, and in the SLD a "null"
*/
public static final String NORMALIZE_NULL_VALUE_IN_COMBOBOX = "-";
final static private Logger LOGGER = LangUtil
.createLogger(FeatureClassification.class);
private String normalizer_field_name;
private DefaultComboBoxModel normlizationAttribsComboBoxModel;
/**
* Caches up to 8 Statistics. Remember, that the {@link DynamicBin1D}
* actually keeps the data in memory!
*/
protected final Map<String, DynamicBin1D> staticStatsCache = Collections
.synchronizedMap(new LimitedHashMap<String, DynamicBin1D>(8));
protected boolean cacheEnabled = true;
/**
* The styled Feature this {@link Classification} works on.
*/
private StyledFeaturesInterface<?> styledFeatures;
/**
* The selected value attribute-field local-name.
*/
protected String value_field_name;
private DefaultComboBoxModel valueAttribsComboBoxModel;
/**
* @param featureSource
* The featuresource to use for the statistics
*/
public FeatureClassification(final StyledFeaturesInterface<?> styledFeatures) {
this(styledFeatures, null, null);
}
/**
* @param featureSource
* The featuresource to use for the statistics
* @param value_field_name
* The column that is used for the classification
*/
public FeatureClassification(
final StyledFeaturesInterface<?> styledFeatures,
final String value_field_name) {
this(styledFeatures, value_field_name, null);
}
/**
* @param featureSource
* The featuresource to use for the statistics
* @param layerFilter
* The {@link Filter} that shall be applied whenever asking for
* the {@link FeatureCollection}. <code>null</code> is not
* allowed, use Filter.INCLUDE
* @param value_field_name
* The column that is used for the classification
* @param normalizer_field_name
* If <code>null</code>, no normalization will be used
*/
public FeatureClassification(StyledFeaturesInterface<?> styledFeatures,
final String value_field_name, final String normalizer_field_name) {
setStyledFeatures(styledFeatures);
setValue_field_name(value_field_name);
setNormalizer_field_name(normalizer_field_name);
}
@Override
public BufferedImage createHistogramImage(boolean showMean, boolean showSd,
int histogramBins, String label_xachsis)
throws InterruptedException, IOException {
HistogramDataset hds = new HistogramDataset();
DoubleArrayList valuesAL;
valuesAL = getStatistics().elements();
// new double[] {0.4,3,4,2,5.,22.,4.,2.,33.,12.}
double[] elements = Arrays.copyOf(valuesAL.elements(), getStatistics()
.size());
hds.addSeries(1, elements, histogramBins);
/** Statically label the Y Axis **/
String label_yachsis = ASUtil
.R("QuantitiesClassificationGUI.Histogram.YAxisLabel");
JFreeChart chart = org.jfree.chart.ChartFactory.createHistogram(null,
label_xachsis, label_yachsis, hds, PlotOrientation.VERTICAL,
false, true, true);
/***********************************************************************
* Paint the classes into the JFreeChart
*/
int countLimits = 0;
for (Double cLimit : getClassLimits()) {
ValueMarker marker = new ValueMarker(cLimit);
XYPlot plot = chart.getXYPlot();
marker.setPaint(Color.orange);
marker.setLabel(String.valueOf(countLimits));
marker.setLabelAnchor(RectangleAnchor.TOP_LEFT);
marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);
plot.addDomainMarker(marker);
countLimits++;
}
/***********************************************************************
* Optionally painting SD and MEAN into the histogram
*/
try {
if (showSd) {
ValueMarker marker;
marker = new ValueMarker(getStatistics().standardDeviation(),
Color.green.brighter(), new BasicStroke(1.5f));
XYPlot plot = chart.getXYPlot();
marker.setLabel(ASUtil
.R("QuantitiesClassificationGUI.Histogram.SD.ShortLabel"));
marker.setLabelAnchor(RectangleAnchor.BOTTOM_LEFT);
marker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
plot.addDomainMarker(marker);
}
if (showMean) {
ValueMarker marker;
marker = new ValueMarker(getStatistics().mean(),
Color.green.darker(), new BasicStroke(1.5f));
XYPlot plot = chart.getXYPlot();
marker.setLabel(ASUtil
.R("QuantitiesClassificationGUI.Histogram.Mean.ShortLabel"));
marker.setLabelAnchor(RectangleAnchor.BOTTOM_LEFT);
marker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT);
plot.addDomainMarker(marker);
}
} catch (Exception e) {
LOGGER.error("Painting SD and MEAN into the histogram", e);
}
/***********************************************************************
* Render the Chart
*/
BufferedImage image = chart.createBufferedImage(400, 200);
return image;
}
/**
* Return a {@link ComboBoxModel} that present all available attributes.
* That excludes the attribute selected in
* {@link #getValueFieldsComboBoxModel()}.
*/
public ComboBoxModel createNormalizationFieldsComboBoxModel() {
normlizationAttribsComboBoxModel = new DefaultComboBoxModel();
normlizationAttribsComboBoxModel
.addElement(NORMALIZE_NULL_VALUE_IN_COMBOBOX);
normlizationAttribsComboBoxModel
.setSelectedItem(NORMALIZE_NULL_VALUE_IN_COMBOBOX);
for (final String fn : FeatureUtil.getNumericalFieldNames(
getStyledFeatures().getSchema(), false)) {
if (fn != valueAttribsComboBoxModel.getSelectedItem())
if (FeatureUtil.checkAttributeNameRestrictions(fn))
normlizationAttribsComboBoxModel.addElement(fn);
else {
LOGGER.info("Hidden attribut " + fn
+ " in createNormalizationFieldsComboBoxModel");
}
else {
// System.out.println("Omittet field" + fn);
}
}
return normlizationAttribsComboBoxModel;
}
/**
* Help the GC to clean up this object.
*/
@Override
public void dispose() {
super.dispose();
}
/**
* @return A combination of StyledFeatures, Value_Field and Norm_Field. This
* String is the Key for the {@link #staticStatsCache}.
*/
protected String getKey() {
return "ID=" + getStyledFeatures().getId() + "TITLE=" + getStyledFeatures().getTitle() + " VALUE="
+ value_field_name + " NORM=" + normalizer_field_name
+ " FILTER=" + getStyledFeatures().getFilter();
}
/**
* @return the name of the {@link Attribute} used for the normalization of
* the value. e.g. value = value field / normalization field
*/
public String getNormalizer_field_name() {
return normalizer_field_name;
}
/**
* This is where the magic happens. Here the attributes of the features are
* summarized in a {@link DynamicBin1D} class.
*
* @throws IOException
*/
@Override
synchronized public DynamicBin1D getStatistics()
throws InterruptedException, IOException {
cancelCalculation.set(false);
if (value_field_name == null)
throw new IllegalArgumentException("value field has to be set");
if (normalizer_field_name == value_field_name)
throw new RuntimeException(
"value field and the normalizer field may not be equal.");
stats = staticStatsCache.get(getKey());
// stats = null;
if (stats == null || !cacheEnabled) {
// Old style.. asking for ALL attributes
// FeatureCollection<SimpleFeatureType, SimpleFeature> features =
// getStyledFeatures()
// .getFeatureCollectionFiltered();
Filter filter = getStyledFeatures().getFilter();
DefaultQuery query = new DefaultQuery(getStyledFeatures()
.getSchema().getTypeName(), filter);
List<String> propNames = new ArrayList<String>();
propNames.add(value_field_name);
if (normalizer_field_name != null)
propNames.add(normalizer_field_name);
query.setPropertyNames(propNames);
FeatureCollection<SimpleFeatureType, SimpleFeature> features = getStyledFeatures()
.getFeatureSource().getFeatures(query);
// Forget about the count of NODATA values
noDataValuesCount.set(0);
final DynamicBin1D stats_local = new DynamicBin1D();
// get the AttributeMetaData for the given attribute to filter
// NODATA values
final AttributeMetadataImpl amd = getStyledFeatures()
.getAttributeMetaDataMap().get(value_field_name);
final AttributeMetadataImpl amdNorm = getStyledFeatures()
.getAttributeMetaDataMap().get(normalizer_field_name);
// // Simulate a slow calculation
// try {
// Thread.sleep(40);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
/**
* Iterating over the values and inserting them into the statistics
*/
final FeatureIterator<SimpleFeature> iterator = features.features();
try {
Double numValue, valueNormDivider;
while (iterator.hasNext()) {
/**
* The calculation process has been stopped from external.
*/
if (cancelCalculation.get()) {
stats = null;
throw new InterruptedException(
"The statistics calculation has been externally interrupted by setting the 'cancelCalculation' flag.");
}
final SimpleFeature f = iterator.next();
// Filter VALUE for NODATA
final Object filtered = amd.fiterNodata(f
.getAttribute(value_field_name));
if (filtered == null) {
noDataValuesCount.incrementAndGet();
continue;
}
numValue = ((Number) filtered).doubleValue();
if (normalizer_field_name != null) {
// Filter NORMALIZATION DIVIDER for NODATA
Object filteredNorm = amdNorm.fiterNodata(f
.getAttribute(normalizer_field_name));
if (filteredNorm == null) {
noDataValuesCount.incrementAndGet();
continue;
}
valueNormDivider = ((Number) filteredNorm)
.doubleValue();
if (valueNormDivider == 0.
|| valueNormDivider.isInfinite()
|| valueNormDivider.isNaN()) {
// Even if it is not defined as a NODATA value,
// division by null is not definied.
noDataValuesCount.incrementAndGet();
continue;
}
numValue = numValue / valueNormDivider;
}
stats_local.add(numValue);
}
stats = stats_local;
if (cacheEnabled)
staticStatsCache.put(getKey(), stats);
} finally {
features.close(iterator);
}
}
return stats;
}
/**
* Remember to apply the associated Filter whenever you access the
* {@link FeatureCollection}
**/
public StyledFeaturesInterface<?> getStyledFeatures() {
return styledFeatures;
}
/**
* @return the name of the {@link Attribute} used for the value. It may
* additionally be normalized if #
*/
public String getValue_field_name() {
return value_field_name;
}
/**
* Return a cached {@link ComboBoxModel} that present all available
* attributes. Its connected to the
*/
public ComboBoxModel getValueFieldsComboBoxModel() {
if (valueAttribsComboBoxModel == null)
valueAttribsComboBoxModel = new DefaultComboBoxModel(FeatureUtil
.getNumericalFieldNames(getStyledFeatures().getSchema(),
false).toArray());
return valueAttribsComboBoxModel;
}
/**
* Will trigger recalculating the statistics including firing events
*/
public void onFilterChanged() {
stats = null;
if (getMethod() == CLASSIFICATION_METHOD.MANUAL) {
fireEvent(new ClassificationChangeEvent(CHANGETYPES.CLASSES_CHG));
} else
calculateClassLimits();
}
/**
* Change the LocalName of the {@link Attribute} that shall be used as a
* normalizer for the value {@link Attribute}. If <code>null</code> is
* passed, the value will not be normalized.
*
* @param normalizer_field_name
* {@link Double}.
*/
public void setNormalizer_field_name(String normalizer_field_name) {
// This max actually be set to null!!
if (this.normalizer_field_name != normalizer_field_name) {
this.normalizer_field_name = normalizer_field_name;
stats = null;
// Das durfte sowieso nie passieren
if (normalizer_field_name == value_field_name) {
normalizer_field_name = null;
throw new IllegalStateException(
"Die GUI sollte nicht erlauben, dass VALUE und NORMALIZATION field gleich sind.");
}
fireEvent(new ClassificationChangeEvent(CHANGETYPES.NORM_CHG));
}
}
public void setStyledFeatures(StyledFeaturesInterface<?> styledFeatures) {
this.styledFeatures = styledFeatures;
}
/**
* Change the LocalName of the {@link Attribute} that shall be used for the
* values. <code>null</code> is not allowed.
*
* @param value_field_name
* {@link Double}.
*/
public void setValue_field_name(final String value_field_name) {
// IllegalArgumentException("null is not a valid value field name");
if ((value_field_name != null)
&& (this.value_field_name != value_field_name)) {
this.value_field_name = value_field_name;
stats = null;
if (normalizer_field_name == value_field_name) {
normalizer_field_name = null;
}
fireEvent(new ClassificationChangeEvent(CHANGETYPES.VALUE_CHG));
}
}
public void setCacheEnabled(boolean cacheEnabled) {
this.cacheEnabled = cacheEnabled;
}
public boolean isCacheEnabled() {
return cacheEnabled;
}
}